/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package dynamicresources

import (
	"sort"
	"testing"

	v1 "k8s.io/api/core/v1"
	resourceapi "k8s.io/api/resource/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	fwk "k8s.io/kube-scheduler/framework"
	st "k8s.io/kubernetes/pkg/scheduler/testing"
	"k8s.io/kubernetes/test/utils/ktesting"
)

func Test_createRequestsAndMappings_requests(t *testing.T) {
	pod1 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName):       "1",
			v1.ResourceName(extendedResourceName + "1"): "2",
		}).
		Obj()
	pod2 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "1"): "2",
		}).
		Obj()

	podInit := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()
	podInit2 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()
	podInit3 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		SidecarReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()

	res := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName): 1,
	}
	res2 := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName):       1,
		v1.ResourceName(extendedResourceName + "1"): 2,
	}
	resInit := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName):          1,
		v1.ResourceName(extendedResourceName + "init"): 2,
	}
	devMap := map[v1.ResourceName]*resourceapi.DeviceClass{
		v1.ResourceName(extendedResourceName): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class",
			},
		},
	}
	devMap2 := map[v1.ResourceName]*resourceapi.DeviceClass{
		v1.ResourceName(extendedResourceName): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class",
			},
		},
		v1.ResourceName(extendedResourceName + "1"): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class1",
			},
		},
	}
	devMapInit := map[v1.ResourceName]*resourceapi.DeviceClass{
		v1.ResourceName(extendedResourceName): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class",
			},
		},
		v1.ResourceName(extendedResourceName + "init"): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "classInit",
			},
		},
	}
	devReq := resourceapi.DeviceRequest{
		Name: "container-0-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           1,
		},
	}
	devReq2 := resourceapi.DeviceRequest{
		Name: "container-0-request-1",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class1",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           2,
		},
	}
	devReq3 := resourceapi.DeviceRequest{
		Name: "container-1-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class1",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           2,
		},
	}
	devReqInit := resourceapi.DeviceRequest{
		Name: "container-1-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           1,
		},
	}
	devReqSidecar := resourceapi.DeviceRequest{
		Name: "container-1-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "classInit",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           1,
		},
	}
	devReq2Init := resourceapi.DeviceRequest{
		Name: "container-1-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "classInit",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           2,
		},
	}
	devReq6Init := resourceapi.DeviceRequest{
		Name: "container-0-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "classInit",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           2,
		},
	}
	devReq3Init := resourceapi.DeviceRequest{
		Name: "container-2-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           1,
		},
	}
	devReq4Init := resourceapi.DeviceRequest{
		Name: "container-3-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "class",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           1,
		},
	}
	devReq5Init := resourceapi.DeviceRequest{
		Name: "container-2-request-0",
		Exactly: &resourceapi.ExactDeviceRequest{
			DeviceClassName: "classInit",
			AllocationMode:  resourceapi.DeviceAllocationModeExactCount,
			Count:           2,
		},
	}

	testcases := map[string]struct {
		pod                *v1.Pod
		extendedResources  map[v1.ResourceName]int64
		cache              fwk.DeviceClassResolver
		wantDeviceRequests []resourceapi.DeviceRequest
	}{
		"nil": {
			pod:                pod1,
			wantDeviceRequests: nil,
		},
		"one resource match": {
			pod:                pod1,
			extendedResources:  res,
			cache:              &mockDeviceClassResolver{mapping: devMap},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq},
		},
		"one resource match, one resource not match": {
			pod:                pod1,
			extendedResources:  res2,
			cache:              &mockDeviceClassResolver{mapping: devMap},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq},
		},
		"two resources match": {
			pod:                pod1,
			extendedResources:  res2,
			cache:              &mockDeviceClassResolver{mapping: devMap2},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq, devReq2},
		},
		"two containers match": {
			pod:                pod2,
			extendedResources:  res2,
			cache:              &mockDeviceClassResolver{mapping: devMap2},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq, devReq3},
		},
		"one init container, one regular container": {
			pod:                podInit,
			extendedResources:  resInit,
			cache:              &mockDeviceClassResolver{mapping: devMapInit},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq6Init, devReqInit},
		},
		"two init containers, one regular container": {
			pod:                podInit2,
			extendedResources:  resInit,
			cache:              &mockDeviceClassResolver{mapping: devMapInit},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReq2Init, devReq3Init},
		},
		"three init containers, one sidecar, one regular container": {
			pod:                podInit3,
			extendedResources:  resInit,
			cache:              &mockDeviceClassResolver{mapping: devMapInit},
			wantDeviceRequests: []resourceapi.DeviceRequest{devReqSidecar, devReq5Init, devReq4Init},
		},
	}

	for name, tc := range testcases {
		t.Run(name, func(t *testing.T) {
			logger, _ := ktesting.NewTestContext(t)
			gotDeviceRequests, _ := createRequestsAndMappings(tc.pod, tc.extendedResources, logger, tc.cache)
			if len(tc.wantDeviceRequests) != len(gotDeviceRequests) {
				t.Fatalf("different length, want %#v, len=%v, got %#v, len=%v", tc.wantDeviceRequests, len(tc.wantDeviceRequests), gotDeviceRequests, len(gotDeviceRequests))
			}
			// gotDeviceRequests should already be sorted by createRequestsAndMappings
			for i, r := range tc.wantDeviceRequests {
				if r.Name != gotDeviceRequests[i].Name {
					t.Errorf("different name, want %#v, got %#v", r, gotDeviceRequests[i])
				}
				if r.Exactly.DeviceClassName != gotDeviceRequests[i].Exactly.DeviceClassName {
					t.Errorf("different deviceClassName, want %#v, got %#v", r, gotDeviceRequests[i])
				}
				if r.Exactly.AllocationMode != gotDeviceRequests[i].Exactly.AllocationMode {
					t.Errorf("different allocationMode, want %#v, got %#v", r, gotDeviceRequests[i])
				}
				if r.Exactly.Count != gotDeviceRequests[i].Exactly.Count {
					t.Errorf("different count, want %#v, got %#v", r.Exactly.Count, gotDeviceRequests[i].Exactly.Count)
				}
			}
		})
	}
}

func Test_createRequestsAndMappings_mappings(t *testing.T) {
	pod1 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName):       "1",
			v1.ResourceName(extendedResourceName + "1"): "2",
		}).
		Obj()
	pod1InitImplicit := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"):                       "1",
			v1.ResourceName(resourceapi.ResourceDeviceClassPrefix + "classInit"): "2",
		}).
		Obj()
	pod2 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "1"): "2",
		}).
		Obj()

	podInit := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()
	podInit2 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()
	podInitImplicit := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(resourceapi.ResourceDeviceClassPrefix + "classInit"): "2",
		}).
		Obj()
	podInit3 := st.MakePod().Name(podName).Namespace(namespace).
		UID(podUID).
		Res(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		SidecarReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "1",
		}).
		InitReq(map[v1.ResourceName]string{
			v1.ResourceName(extendedResourceName + "init"): "2",
		}).
		Obj()
	res := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName):       1,
		v1.ResourceName(extendedResourceName + "1"): 2,
	}
	resInit := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName):          1,
		v1.ResourceName(extendedResourceName + "init"): 2,
	}
	resInitImplicit := map[v1.ResourceName]int64{
		v1.ResourceName(extendedResourceName):                                1,
		v1.ResourceName(extendedResourceName + "init"):                       2,
		v1.ResourceName(resourceapi.ResourceDeviceClassPrefix + "classInit"): 2,
	}
	devMap := map[v1.ResourceName]*resourceapi.DeviceClass{
		v1.ResourceName(extendedResourceName): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class",
			},
		},
		v1.ResourceName(extendedResourceName + "1"): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class1",
			},
		},
	}
	devMapInit := map[v1.ResourceName]*resourceapi.DeviceClass{
		v1.ResourceName(extendedResourceName): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "class",
			},
		},
		v1.ResourceName(extendedResourceName + "init"): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "classInit",
			},
		},
		v1.ResourceName(resourceapi.ResourceDeviceClassPrefix + "classInit"): {
			ObjectMeta: metav1.ObjectMeta{
				Name: "classInit",
			},
		},
	}
	cer := v1.ContainerExtendedResourceRequest{
		ContainerName: "con0",
		ResourceName:  extendedResourceName,
		RequestName:   "container-0-request-0",
	}
	cer1 := v1.ContainerExtendedResourceRequest{
		ContainerName: "con0",
		ResourceName:  extendedResourceName + "1",
		RequestName:   "container-0-request-1",
	}
	cer2 := v1.ContainerExtendedResourceRequest{
		ContainerName: "con1",
		ResourceName:  extendedResourceName + "1",
		RequestName:   "container-1-request-0",
	}
	cer3 := v1.ContainerExtendedResourceRequest{
		ContainerName: "con0",
		ResourceName:  extendedResourceName,
		RequestName:   "container-1-request-0",
	}
	cer4 := v1.ContainerExtendedResourceRequest{
		ContainerName: "con0",
		ResourceName:  extendedResourceName,
		RequestName:   "container-2-request-0",
	}
	cer5 := v1.ContainerExtendedResourceRequest{
		ContainerName: "con0",
		ResourceName:  extendedResourceName,
		RequestName:   "container-3-request-0",
	}
	cerInit := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-1-request-0",
	}
	cerInit0 := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-0-request-0",
	}
	cerInit1 := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-1-request-0",
	}
	cerInit2 := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con1",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-1-request-0",
	}
	cerInit3 := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con2",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-2-request-0",
	}
	cerSidecar := v1.ContainerExtendedResourceRequest{
		ContainerName: "sidecar-con1",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-1-request-0",
	}

	cerInitImplicit := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-0-request-0",
	}
	cerInit4Implicit := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  extendedResourceName + "init",
		RequestName:   "container-0-request-1",
	}
	cerInit2Implicit := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con1",
		ResourceName:  resourceapi.ResourceDeviceClassPrefix + "classInit",
		RequestName:   "container-1-request-0",
	}
	cerInit3Implicit := v1.ContainerExtendedResourceRequest{
		ContainerName: "init-con0",
		ResourceName:  resourceapi.ResourceDeviceClassPrefix + "classInit",
		RequestName:   "container-0-request-0",
	}

	testcases := map[string]struct {
		pod                *v1.Pod
		extnededResources  map[v1.ResourceName]int64
		deviceClassMapping fwk.DeviceClassResolver
		wantReqMappings    []v1.ContainerExtendedResourceRequest
	}{
		"one container, two requests": {
			pod:                pod1,
			extnededResources:  res,
			deviceClassMapping: &mockDeviceClassResolver{devMap},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cer, cer1},
		},
		"one container, one explicit and one implicit request": {
			pod:                pod1InitImplicit,
			extnededResources:  resInitImplicit,
			deviceClassMapping: &mockDeviceClassResolver{devMapInit},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cerInit3Implicit, cerInit4Implicit},
		},
		"two containers, two requests": {
			pod:                pod2,
			extnededResources:  res,
			deviceClassMapping: &mockDeviceClassResolver{devMap},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cer, cer2},
		},
		"one init container, one regular container, one request": {
			pod:                podInit,
			extnededResources:  resInit,
			deviceClassMapping: &mockDeviceClassResolver{devMapInit},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cerInit0, cer3},
		},
		"three containers (two are init container), two requests": {
			pod:                podInit2,
			extnededResources:  resInit,
			deviceClassMapping: &mockDeviceClassResolver{devMapInit},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cerInit, cerInit2, cer4},
		},
		"three containers (two are init container), both explicit and implicit resources": {
			pod:                podInitImplicit,
			extnededResources:  resInitImplicit,
			deviceClassMapping: &mockDeviceClassResolver{devMapInit},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cerInitImplicit, cerInit2Implicit, cer4},
		},
		"four containers (two are init container, one sidecar), three requests": {
			pod:                podInit3,
			extnededResources:  resInit,
			deviceClassMapping: &mockDeviceClassResolver{devMapInit},
			wantReqMappings:    []v1.ContainerExtendedResourceRequest{cerInit1, cerSidecar, cerInit3, cer5},
		},
	}

	for name, tc := range testcases {
		t.Run(name, func(t *testing.T) {
			logger, _ := ktesting.NewTestContext(t)
			_, gotReqMappings := createRequestsAndMappings(tc.pod, tc.extnededResources, logger, tc.deviceClassMapping)
			if len(tc.wantReqMappings) != len(gotReqMappings) {
				t.Fatalf("different length, want %#v, got %#v", tc.wantReqMappings, gotReqMappings)
			}
			sort.Slice(gotReqMappings, func(i, j int) bool {
				if gotReqMappings[i].RequestName < gotReqMappings[j].RequestName {
					return true
				}
				if gotReqMappings[i].RequestName > gotReqMappings[j].RequestName {
					return false
				}
				return gotReqMappings[i].ContainerName < gotReqMappings[j].ContainerName
			})
			for i, r := range tc.wantReqMappings {
				if r.RequestName != gotReqMappings[i].RequestName {
					t.Errorf("different request name, want %#v, got %#v", r, gotReqMappings[i])
				}
				if r.ContainerName != gotReqMappings[i].ContainerName {
					t.Errorf("different container name, want %#v, got %#v", r, gotReqMappings[i])
				}
				if r.ResourceName != gotReqMappings[i].ResourceName {
					t.Errorf("different resource name, want %#v, got %#v", r, gotReqMappings[i])
				}
			}
		})
	}
}
